// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Reflection;

namespace SnapObjects.Data
{
    internal class PocoModelMeta : IPocoModelMeta
    {
        public static readonly int TABLE_ID = 0; //TableAttribute

        protected readonly ModelAttributeInfo _attribute;

        private readonly IReadOnlyDictionary<string, IPropertyMeta> _propertyMetaMap;

        public PocoModelMeta(Type modelType)
        {
            this.ModelType = modelType;

            var properties = modelType.GetProperties(
                BindingFlags.Public | BindingFlags.Instance).OrderBy(m => m.MetadataToken).ToList();

            var propertyList = new List<IPropertyMeta>();
            var propertyMap = new Dictionary<string, IPocoProperty>(
                                    StringComparer.OrdinalIgnoreCase);

            for (var i = 0; i < properties.Count; i++)
            {
                var property = properties[i];
                var meta = this.CreatePropertyMeta(i, property);

                propertyList.Add(meta);
                propertyMap.Add(property.Name, meta);
            }

            this.Properties = propertyList;
            this.PropertyMap = propertyMap;

            this.SqlParameters = new List<ISqlParameter>();
            this.SqlWheres = new List<ISqlWhere>();
            this.SqlHavings = new List<ISqlHaving>();
            this.SubQueries = new List<ISubQuery>();
            this.SqlUnions = new List<ISqlUnion>();
            this.SqlSelects = new Dictionary<string, string>();
            this.Tables = new List<TableAttribute>();

            var attributes = modelType.GetAttributes();

            if (attributes.Count > 0)
            {
                _attribute = this.CreateModelAttributeInfo(attributes);

                this.SqlParameters = _attribute.SqlParameters;
                this.SqlWheres = _attribute.SqlWheres;
                this.SqlHavings = _attribute.SqlHavings;
                this.Tables = _attribute.Tables;
                this.SubQueries = _attribute.SubQueries;
                this.SqlUnions = _attribute.SqlUnions;
                this.SqlSelects = _attribute.SqlSelects;

                this.TableDefined = _attribute.TableDefined;
                this.Distinct = _attribute.Distinct;
                this.TopDefinded = _attribute.TopDefinded;
                this.TopRows = _attribute.TopRows;

                if (_attribute.TableDefined)
                {
                    this.MappedTable = _attribute.Table;
                    this.MappedSchema = _attribute.Schema;
                }
                else if (_attribute.FromTables.Count > 0)
                {
                    var table = _attribute.FromTables.First();

                    this.MappedTable = table.Name;
                    this.MappedSchema = table.Schema;

                    if (table.AliasDefined)
                    {
                        this.MappedTableAlias = table.Alias;
                    }
                }
                else
                {
                    this.MappedTable = modelType.Name;
                }
            }
            else
            {
                this.MappedTable = modelType.Name;
            }

            this.HasEmbeddedProperties = this.EmbeddedProperties.Count > 0;
            this.HasCascadeCreateProperties = this.CascadeCreateProperties.Count > 0;
            this.HasCascadeDeleteProperties = this.CascadeDeleteProperties.Count > 0;
            this.HasSetValueProperties = this.SetValueProperties.Count > 0;

            this.UpdatableProperties = this.PropertyMap.Values.Where(o => !o.IsNotMapped &&
                                                            !o.IsEmbedded &&
                                                            !o.IsForeign &&
                                                            o.SqlCompute == null)
                                                .OrderBy(o => o.Index)
                                                .ToList();

            this.UpdateWhereStrategy = _attribute?.UpdateWhereStrategy == null ? 
                                        UpdateWhereStrategy.KeyColumns : 
                                        _attribute.UpdateWhereStrategy.UpdateWhereStrategy;

            _propertyMetaMap = this.PropertyMap.ToDictionary(m => m.Key, m => (IPropertyMeta)m.Value); 
        }

        protected PocoModelMeta()
        {

        }

        protected virtual ModelAttributeInfo CreateModelAttributeInfo(IReadOnlyList<Attribute> attributes)
        {
            return new ModelAttributeInfo(attributes);
        }

        protected virtual PocoPropertyMeta CreatePropertyMeta(int index, PropertyInfo propertyInfo)
        {
            return new PocoPropertyMeta(this, index, propertyInfo);
        }

        public int PropertyCount => this.Properties.Count;

        public string NameOf(int index)
        {
            return this.Properties.Count > index
                ? this.Properties[index].Name : String.Empty;
        }

        public int IndexOf(string name)
        {
            if (this.PropertyMap.TryGetValue(name, out var property))
            {
                return property.Index;
            }
            else
            {
                throw new NotPropertyException(name);
            }
        }

        public string GetFullTableName(int tableId)
        {
            if (tableId == TABLE_ID)
            {
                return String.IsNullOrWhiteSpace(this.MappedSchema) ?
                                this.MappedTable : this.MappedSchema + "." + this.MappedTable;
            }
            else
            {
                var joinTable = _attribute.JoinTables.Where(o => o.Id == tableId).FirstOrDefault();

                if (joinTable != null)
                {
                    return String.IsNullOrWhiteSpace(joinTable.Schema) ?
                                joinTable.Name : joinTable.Schema + "." + joinTable.Name;
                }
                else
                {
                    var fromTable = _attribute.FromTables.Where(o => o.Id == tableId).FirstOrDefault();

                    if (fromTable != null)
                    {
                        return String.IsNullOrWhiteSpace(fromTable.Schema) ?
                                    fromTable.Name : fromTable.Schema + "." + fromTable.Name;
                    }
                    else
                    {
                        throw new InvalidOperationException("Invalid table id, tableId: " + tableId);
                    }
                }
            }
        }

        public IReadOnlyList<ISqlParameter> CreateParametersFromKeys()
        {
            var sqlParameters = new List<ISqlParameter>();

            foreach (var key in this.KeyProperties)
            {
                sqlParameters.Add(new SqlParameterAttribute(key.Name.ToLower(), key.DataType));
            }

            return sqlParameters;
        }

        public bool IsEmbedded(string propertyName)
        {
            return this.PropertyMap.TryGetValue(propertyName, out var property) ?
                                    property.IsEmbedded : false;
        }

        public bool IsCascadeCreate(string propertyName)
        {
            return this.PropertyMap.TryGetValue(propertyName, out var property) ?
                                    property.IsCascadeCreate : false;
        }

        public bool IsCascadeDelete(string propertyName)
        {
            return this.PropertyMap.TryGetValue(propertyName, out var property) ?
                                    property.IsCascadeDelete : false;
        }

        public bool IsContainerType(string propertyName)
        {
            return this.PropertyMap.TryGetValue(propertyName, out var property) ?
                                    property.IsContainerType : false;
        }

        public bool IsArray(string propertyName)
        {
            return this.PropertyMap.TryGetValue(propertyName, out var property) ?
                                    property.DataType.IsArray : false;
        }

        public bool Contains(string name)
        {
            return this.PropertyMap.ContainsKey(name);
        }

        public IReadOnlyList<IPocoProperty> KeyProperties =>
                        this.PropertyMap.Values.Where(o => o.IsKey).ToList();

        public IReadOnlyList<IPocoProperty> EmbeddedProperties =>
                        this.PropertyMap.Values.Where(o => o.IsEmbedded).ToList();

        public IReadOnlyList<IPocoProperty> CascadeCreateProperties =>
                        this.PropertyMap.Values.Where(o => o.IsCascadeCreate).ToList();

        public IReadOnlyList<IPocoProperty> CascadeDeleteProperties =>
                        this.PropertyMap.Values.Where(o => o.IsCascadeDelete).ToList();

        public IReadOnlyList<IPocoProperty> SetValueProperties =>
                            this.PropertyMap.Values.Where(o => o.HasSetValueOperation).ToList();


        public IReadOnlyList<IPocoProperty> DefaultValueProperties =>
            this.PropertyMap.Values.Where(o => o.Attribute.DefaultValueDefined).ToList();

        public bool HasEmbeddedProperties { get; }

        public bool HasCascadeCreateProperties { get; }

        public bool HasCascadeDeleteProperties { get; }

        public bool HasSetValueProperties { get; }
        public Type ModelType { get; }

        public string MappedTable { get; protected set; }

        public string MappedTableAlias { get; protected set; }

        public string MappedSchema { get; protected set; }

        public IReadOnlyList<IPropertyMeta> Properties { get; }

        public IReadOnlyDictionary<string, IPocoProperty> PropertyMap { get; }

        public virtual IReadOnlyList<IPocoProperty> UpdatableProperties { get; }

        public IReadOnlyList<ISqlParameter> SqlParameters { get; }

        public IReadOnlyList<TableAttribute> Tables { get; }

        public IReadOnlyList<ISubQuery> SubQueries { get; }
        public IReadOnlyList<ISqlWhere> SqlWheres { get; }

        public IReadOnlyList<ISqlHaving> SqlHavings { get; }

        public ISqlGroupBy SqlGroupBy => _attribute?.SqlGroupBy;

        public ISqlOrderBy SqlOrderBy => _attribute?.SqlOrderBy;

        public IReadOnlyList<ISqlUnion> SqlUnions { get; }

        public IReadOnlyDictionary<string, string> SqlSelects { get; }

        public UpdateWhereStrategy UpdateWhereStrategy { get; protected set; }

        public bool Distinct { get; }

        public bool TableDefined { get; }

        public bool HasFromTable => _attribute?.FromTables.Count > 0;

        public bool HasJoinTable => _attribute?.JoinTables.Count > 0;

        public bool TopDefinded { get; }

        public int TopRows { get; }

        IReadOnlyList<IPropertyMeta> IModelMeta.KeyProperties => this.KeyProperties;

        IReadOnlyList<IUpdatableProperty> IModelMeta.UpdatableProperties => this.UpdatableProperties;

        IReadOnlyDictionary<string, IPropertyMeta> IModelMeta.PropertyMap => _propertyMetaMap;
    }
}
